function gui()
	pkg load signal;
	clear;
	EQ = [];
	Fs = get_config().Fs;
	nyquist_f = Fs/2;
	fn = 1;
	% create figure and panel on it
	fn = figure(fn, "toolbar", "none", "menubar", "none", 'Position', [200 200 1500 700]); fn = fn + 1;
	EQ_right = uipanel ("title", "EQ Right", "position", [0, 0, 1, 0.5]);
	EQ_left = uipanel ("title", "EQ Left", "position", [0, 0.5, 1, 0.5]);
	main_panes = [struct("pane", EQ_left); struct("pane", EQ_right)];

	% UI panel relative position table
	positions = [
		[0    0   0.25 0.5 ]
		[0.25 0   0.5  0.5 ]
		[0.5  0   0.75 0.5 ]
		[0.75 0   1    0.5 ]
		[0    0.5 0.25 1   ]
		[0.25 0.5 0.5  1   ]
		[0.5  0.5 0.75 1   ]
		[0.75 0.5 1    1   ]
	];

	fn = figure(fn); fn = fn + 1;
	h = [0 0];
	w = [1 nyquist_f];
	n_octaves = 10;
	subplot(2, 1, 1);
	main_panes(1).plot = plot(log2(w), h, "linewidth", 4);
	% Range set to match http://audio-tuning.appspot.com/
	axis([log2(nyquist_f / (2 ^ n_octaves)), log2(nyquist_f), -24, 18]);
	grid on;
	subplot(2, 1, 2);
	main_panes(2).plot = plot(log2(w), h, "linewidth", 4);
	% Range set to match http://audio-tuning.appspot.com/
	axis([log2(nyquist_f / (2 ^ n_octaves)), log2(nyquist_f), -24, 18]);
	grid on;
	% TODO add ability to switch between octaves and log(Hz)
	for c = 1:2
		for i = 1:length(positions)
			EQ = initialize_biquad(i, c, positions(i, 1:end), main_panes, Fs, EQ);
		endfor
	endfor
	% octave is silly and doesn't support references so we have to re-write the
	% control matrix so the callbacks have access without using globals
	for c = 1:2
		for i = 1:length(positions)
			update_control_elements(EQ(i, c), EQ);
		endfor
	endfor
	fn = figure(fn, "toolbar", "none", "menubar", "none", 'Position', [200 200 300 220]); fn = fn + 1;
	menu_window = struct();
	menu_window.ip_box = uicontrol( ...
		"style", "edit", ...
		"position", [120 190 180 30]);
	menu_window.ip_label = uicontrol( ...
		"style", "text",
		"string", "DUT IP:", ...
		"position", [0 190 120 30]);
	menu_window.user_box = uicontrol( ...
		"style", "edit", ...
		"position", [120 160 180 30]);
	menu_window.user_label = uicontrol( ...
		"style", "text",
		"string", "DUT User:", ...
		"position", [0 160 120 30]);
	menu_window.device_box = uicontrol( ...
		"style", "edit", ...
		"position", [120 130 180 30]);
	menu_window.device_label = uicontrol( ...
		"style", "text",
		"string", "Device Num:", ...
		"position", [0 130 120 30]);
	menu_window.alsa_box = uicontrol( ...
		"style", "edit", ...
		"position", [120 100 180 30]);
	menu_window.alsa_label = uicontrol( ...
		"style", "text",
		"string", "ALSA Ctl Num:", ...
		"position", [0 100 120 30]);
	menu_window.upload = uicontrol( ...
		"style", "pushbutton", ...
		'string', 'Push config to DUT', ...
		"callback", @push_config, ...
		"position", [0 50 300 50], ...
		"userdata", struct("menu_window", menu_window, "EQ", EQ));
	menu_window.load_dsp = uicontrol( ...
		"style", "pushbutton", ...
		'string', 'Load dsp.ini', ...
		"callback", @eq_load_dsp_ini, ...
		"position", [0 0 300 50], ...
		"userdata", struct("EQ", EQ));
end

function conf = get_config()
  % since octave has no definitive way to have constants we are going to hack a
  % struct into behaving like one
  conf = struct( ...
      "Fs", 48000 ...
      );
end

function set_udata_field(h, field, value)
	udata = get(h, "userdata");
	udata = setfield(udata, field, value);
	set(h, "userdata", udata);
end

function peq = build_peq(channel, EQ)
	peq = [];
	for i = 1:8
		biquad = EQ(i, channel);
		if get(biquad.enable_switch, "value") == 1
			type = get(biquad.type.dropdown, "value");
			freq = str2num(get(biquad.freq.edit, "string"));
			Q = str2double(get(biquad.Q.edit, "string"));
			gain = str2double(get(biquad.gain.edit, "string"));
			% dropdown maps straight to filter index
			% filter only uses needed arguments, all others will be ignored
			peq = [peq; type, freq, gain, Q];
		endif
	endfor
end

function update_plot(channel, main_panes, EQ)
	Fs = get_config().Fs;
	nyquist_f = Fs / 2;
	pl = main_panes(channel).plot;
	spectrum_size = 1000;
	n = logspace(log10(1), log10(nyquist_f), spectrum_size);
	peq = build_peq(channel, EQ);
	[z p k] = eq_define_parametric_eq(peq, Fs);
	% octave gets angry when you pile up poles and zeros
	[num den] = zp2tf(z, p, k);
	[h, w] = freqz(num,den, n, Fs);
	h = 20*log10(h);
	set(pl, "YData", h, "XData", log2(w));
end

function set_section_visibility(section, visible)
	for [control, control_name] = section
		set(control, "visible", visible);
	endfor
end

function dropdown_callback(h, event)
	udata = get(h, "userdata");
	EQ = udata.EQ;
	biquad  = EQ(udata.index, udata.channel);
	eq = eq_defaults();
	switch (get(h, "value"))
		case eq.PEQ_HP1
			% highpass 1st order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_HP2
			% highpass 2nd order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_LP1
			% lowpass 1st order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_LP2
			% lowpass 2nd order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_LS1
			% lowshelf 1st order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "on");
		case eq.PEQ_LS2
			% lowhelf 2nd order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "on");
		case eq.PEQ_HS1
			% highshelf 1st order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "on");
		case eq.PEQ_HS2
			% highshelf 2nd order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "on");
		case eq.PEQ_PN2
			% peaking
			set_section_visibility(biquad.Q, "on");
			set_section_visibility(biquad.gain, "on");
		case eq.PEQ_LP4
			% lowpass 4th order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_HP4
			% highpass 4th order
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_LP2G
			% lowpass 2nd order with resonnance
			set_section_visibility(biquad.Q, "on");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_HP2G
			% highpass 2nd order with resonnance
			set_section_visibility(biquad.Q, "on");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_BP2
			% bandpass
			set_section_visibility(biquad.Q, "on");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_NC2
			% notch
			set_section_visibility(biquad.Q, "on");
			set_section_visibility(biquad.gain, "off");
		case eq.PEQ_LS2G
			% lowshelf, CRAS implementation
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "on");
		case eq.PEQ_HS2G
			% highshelf, CRAS implementation
			set_section_visibility(biquad.Q, "off");
			set_section_visibility(biquad.gain, "on");
	endswitch
	update_plot(udata.channel, udata.main_panes, EQ);
end

function enable_callback(h, event)
	off = get(h, "value") == 0;
	if off
		visible = "off";
	else
		visible = "on";
	endif
	udata = get(h, "userdata");
	EQ = udata.EQ;
	biquad  = EQ(udata.index, udata.channel);
	for [section, section_name] = biquad
		if strcmp(section_name, "enable_switch") || strcmp(section_name, "pane")
			continue
		endif
		set_section_visibility(section, visible);
	endfor
	if !off
		dropdown_callback(biquad.type.dropdown,0);
	endif
	update_plot(udata.channel, udata.main_panes, EQ);
end

function slider_callback(h, event)
	udata = get(h, "userdata");
	EQ = udata.EQ;
	editbox  = getfield(EQ(udata.index, udata.channel), udata.prefix).edit;
	val = get(h, "value");
	if udata.is_log
		val = 0.1*2^val;
	endif
	set(editbox, "string", num2str(val));
	update_plot(udata.channel, udata.main_panes, EQ);
end

function edit_callback(h, event)
	udata = get(h, "userdata");
	EQ = udata.EQ;
	slider  = getfield(EQ(udata.index, udata.channel), udata.prefix).slider;
	val = str2num(get(h, "string"));
	if udata.is_log && val != 0
		val = log2(val/0.1);
	end
	set(slider, "value", val);
	update_plot(udata.channel, udata.main_panes, EQ);
end

function EQ = initialize_biquad(index, channel, position, main_panes, Fs, EQ)
	nyquist_f = Fs / 2;
	% this string matches the order defined in eq.PEQ_*, do not change the order
	eq_strings = {"highpass 1st order", "highpass 2nd order", ...
		"lowpass 1st order", "lowpass 2nd order", "lowshelf 1st order", ...
		"lowshelf 2nd order", "highshelf 1st order", "highshelf 2nd order", ...
		"peaking 2nd order", "lowpass 4th order", "highpass 4th order", ...
		"lowpass 2nd order (Google)", "highpass 2nd order (Google)", ...
		"bandpass 2nd order", "notch 2nd order", "lowshelf 2nd order (Google)", ...
		"highshelf 2nd order (Google)"};
	eq = eq_defaults();
	% UI panels, we have to use absolute positioning
	%
	% +-------+-----------+-------+--+--+--+--+--+----------+
	% | Label | Dropdown  |       |  |  |  |  |  | checkbox |
	% +-------+-----------+-------+--+--+--+--+--+----------+
	% | Label | input box | label | slider                  |
	% +-------+-----------+-------+-------------------------+
	% | Label | input box | label | slider                  |
	% +-------+-----------+-------+-------------------------+
	% | Label | input box | label | slider                  |
	% +-------+-----------+-------+-------------------------+

	EQ(index, channel).pane = uipanel ("parent", main_panes(channel).pane, "position", position);
	p = EQ(index, channel).pane;
	% octave is silly and doesn't let one position controls relatively inside a uipanel
	EQ(index, channel).gain.label = uicontrol( ...
		"style", "text", ...
		"parent", p, ...
		"position", [0 10 40 30], ...
		"visible", "off", ...
		"string", "Gain");
	EQ(index, channel).gain.edit = uicontrol( ...
		"style", "edit", ...
		"parent", p, ...
		"position", [40 10 90 30], ...
		"string", "0", ...
		"userdata", struct( ...
			"index", index, ...
			"channel", channel, ...
			"prefix", "gain", ...
			"is_log", false, ...
			"EQ", EQ, ...
			"main_panes", main_panes), ...
		"visible", "off", ...
		"callback", @edit_callback);
	EQ(index, channel).gain.units = uicontrol( ...
		"style", "text", ...
		"parent", p, ...
		"string", "dB", ...
		"visible", "off", ...
		"position", [140 10 17 30]);
	EQ(index, channel).gain.slider = uicontrol( ...
		"style", "slider", ...
		"parent", p, ...
		"position", [160 10 200 30], ...
		"min", -40, ...
		"max", 40, ...
		"userdata", struct(
			"index", index, ...
			"channel", channel, ...
			"prefix", "gain", ...
			"is_log", false, ...
			"EQ", EQ, ...
			"main_panes", main_panes), ...
		"visible", "off", ...
		"callback", @slider_callback);

	EQ(index, channel).Q.label = uicontrol( ...
		"style", "text", ...
		"parent", p, ...
		"position", [0 50 50 30], ...
		"visible", "off", ...
		"string", "Q");
	EQ(index, channel).Q.edit = uicontrol( ...
		"style", "edit", ...
		"parent", p, ...
		"position", [40 50 90 30], ...
		"string", "1", ...
		"userdata", struct( ...
			"index", index, ...
			"channel", channel, ...
			"prefix", "Q", ...
			"is_log", true, ...
			"EQ", EQ, ...
			"main_panes", main_panes), ...
		"visible", "off", ...
		"callback", @edit_callback);
	EQ(index, channel).Q.slider = uicontrol( ...
		"style", "slider",                 ...
		"parent", p,                       ...
		"position", [160 50 200 30],       ...
		"min", 0,                          ...
		"max", log2(1000/0.1),             ...
		"value", log2(1/0.1),              ...
		"userdata", struct(
			"index", index, ...
			"channel", channel, ...
			"prefix", "Q", ...
			"is_log", true, ...
			"EQ", EQ, ...
			"main_panes", main_panes), ...
		"visible", "off", ...
		"callback", @slider_callback);

	EQ(index, channel).freq.label = uicontrol( ...
		"style", "text", ...
		"parent", p, ...
		"position", [0 90 40 30], ...
		"visible", "off", ...
		"string", "Freq");
	EQ(index, channel).freq.edit = uicontrol( ...
		"style", "edit", ...
		"parent", p, ...
		"position", [40 90 90 30], ...
		"string", "350", ...
		"userdata", struct( ...
			"index", index, ...
			"channel", channel, ...
			"prefix", "freq", ...
			"is_log", true, ...
			"EQ", EQ, ...
			"main_panes", main_panes), ...
		"visible", "off", ...
		"callback", @edit_callback);
	EQ(index, channel).freq.units = uicontrol( ...
		"style", "text", ...
		"parent", p, ...
		"position", [140 90 17 30], ...
		"visible", "off", ...
		"string", "Hz");
	EQ(index, channel).freq.slider   = uicontrol( ...
		"style", "slider", ...
		"parent", p, ...
		"position", [160 90 200 30], ...
		"min", 1, ...
		"max", log2(nyquist_f/0.1), ...
		"value", log2(350/0.1), ...
		"userdata", struct(
			"index", index, ...
			"channel", channel, ...
			"prefix", "freq", ...
			"is_log", true, ...
			"EQ", EQ, ...,
			"main_panes", main_panes), ...
		"visible", "off", ...
		"callback", @slider_callback);

	EQ(index, channel).type.label = uicontrol( ...
		"style", "text", ...
		"parent", p, ...
		"position", [0 130 40 30], ...
		"visible", "off", ...
		"string", "Type");
	EQ(index, channel).type.dropdown = uicontrol( ...
		"style", "popupmenu", ...
		"parent", p, ...
		"position", [40 130 240 30], ...
		"string", eq_strings, ...
		"value", eq.PEQ_PN2, ...
		"userdata", struct( ...
			"index", index, ...
			"channel", channel, ...
			"EQ", EQ, ...
			"main_panes", main_panes), ...
		"visible", "off", ...
		"callback", @dropdown_callback);
	EQ(index, channel).enable_switch = uicontrol( ...
		"style", "checkbox", ...
		"parent", p, ...
		"position", [350 130 30 30], ...
		"value", 0, ...
		"userdata", struct(
			"index", index, ...
			"channel", channel, ...
			"EQ", EQ, ...
			"main_panes", main_panes), ...
		"callback", @enable_callback);
end

function push_config(h, evnt)
	udata = get(h, "userdata");
	window = udata.menu_window;
	EQ = udata.EQ;
	ip = get(window.ip_box, "string");
	user = get(window.user_box, "string");
	device = str2num(get(window.device_box, "string"));
	alsa_num = str2num(get(window.alsa_box, "string"));
	target = struct("ip", ip, "user", user, "device", device, "control", alsa_num);

	temp_file = tempname();

	eq_left = eq_defaults();
	eq_left.enable_iir = 1;
	eq_left.peq = build_peq(1, EQ);
	eq_left.norm_type = 'peak';
	eq_left.norm_offs_db = 0;
	eq_left.fs = get_config().Fs;

	eq_right = eq_defaults();
	eq_right.enable_iir = 1;
	eq_right.peq = build_peq(2, EQ);
	eq_right.norm_type = 'peak';
	eq_right.norm_offs_db = 0;
	eq_right.fs = get_config().Fs;

	eq_left  = eq_compute(eq_left);
	eq_right = eq_compute(eq_right);

	bq_left  = eq_iir_blob_quant(eq_left.p_z,  eq_left.p_p,  eq_left.p_k);
	bq_right = eq_iir_blob_quant(eq_right.p_z, eq_right.p_p, eq_right.p_k);

	channels_in_config = 2;
	assign_response = [0 1];
	num_responses = 2;
	bm = eq_iir_blob_merge(channels_in_config, ...
						   num_responses, ...
						   assign_response, ...
						   [bq_left bq_right]);

	bp = eq_iir_blob_pack(bm);
	eq_alsactl_write(temp_file, bp);
	eq_deploy_to_dut(target, temp_file);
end

function update_control_elements(section, EQ)
	set_udata_field(section.gain.edit, "EQ", EQ);
	set_udata_field(section.gain.slider, "EQ", EQ);
	set_udata_field(section.Q.edit, "EQ", EQ);
	set_udata_field(section.Q.slider, "EQ", EQ);
	set_udata_field(section.freq.edit, "EQ", EQ);
	set_udata_field(section.freq.slider, "EQ", EQ);
	set_udata_field(section.type.dropdown, "EQ", EQ);
	set_udata_field(section.enable_switch, "EQ", EQ);
end

